<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .sullivan-grid {
            margin-left: 100px;
            margin-top: 100px;
            width: 600px;
            height: 600px;
            border: 1px solid red;
            display: grid;
            gap: 6px;
            .sullivan-grid-item {
                position: relative;
                border: 1px solid blue;
            }
            .sullivan-grid-item-content {
                position: absolute;
                width: 100%;
                height: 100%;
                border: 1px solid blue;
            }
        }
        .sullivan-grid-item-header {
            grid-area: header;
        }
        .sullivan-grid-item-direction {
            background: rgba(0, 0, 0, .3)
        }
        .sullivan-grid-item-top,
        .sullivan-grid-item-bottom {
            position: absolute;
            height: 10px;
            width: 100%;
            cursor: row-resize;
        }
        .sullivan-grid-item-right,
        .sullivan-grid-item-left {
            position: absolute;
            width: 10px;
            height: 100%;
            cursor: col-resize;
        }
        .sullivan-grid-item-top {
            top: -3px;
        }
        .sullivan-grid-item-right {
            right: -3px;
        }
        .sullivan-grid-item-bottom {
            bottom: -3px;
        }
        .sullivan-grid-item-left {
            left: -3px;
        }
    </style>
</head>
<body>
<div id="container"></div>
<script src="./jquery.js"></script>
<script>
    class Grid {
        $grid;
        row;
        col;
        areas;
        areasCopy;
        constructor([row, col] = [1, 1]) {
            const className = `sullivan-grid sullivan-grid-${row}x${col}`;
            this.$grid = $(`<div class="${className}" style="display: grid;" ></div>`);
            this.row = row;
            this.col = col;
        }

        createGridArea () {
            const { row, col } = this;
            const areas = [];
            for (let i = 0; i < row; i++) {
                for (let j = 0; j < col; j++) {
                    areas.push(`grid-gap-${i}x${j}`)
                }
            }
            this.areas = areas
            this.areasCopy = [...areas]
            return areas;
        }

        update (index, area) {
            this.areasCopy[index] = area;
            this.setGridArea()
        }

        createGrid(grid = [1, 1], gap = [6, 6]) {
            const { row, col } = this;
            const [rowGap, columnGap] = gap;
            const rowStyle = `repeat(${row}, 1fr)`;
            const colStyle = `repeat(${col}, 1fr)`;

            this.$grid.css({
                "grid-template-rows": rowStyle,
                "grid-template-columns": colStyle,
                "grid-row-gap": rowGap,
                "grid-column-gap": columnGap,
            });

            const areas = this.createGridArea(grid);
            const createDirection = (direction) => (
                `<div class="sullivan-grid-item-direction sullivan-grid-item-${direction}"></div>`
            );

            areas.forEach((area, i) => {
                const $gridItem = $(
                    `<div class="sullivan-grid-item ${area}" data-index="${i + 1}" style="grid-area:
                        ${area}"><div class="sullivan-grid-item-content"></div></div>`
                );
                $gridItem.find('.sullivan-grid-item-content').append(['top', 'right', 'bottom', 'left'].map(createDirection))
                this.$grid.append($gridItem);
            })

            this.setGridArea()
            return this.$grid;
        }

        setGridArea () {
            let tempAreas = ``

            this.areasCopy.forEach((item, index) => {
                if (index % this.row || index === 0) {
                    tempAreas += `${item} `
                } else {
                    tempAreas += `" "${item} `
                }
            })

            // console.log(this.areas)
            // console.log(`"${tempAreas}"`)
            this.$grid.css({
                "grid-template-areas": `"${tempAreas}"`
            });
        }
    }

    const grid = new Grid([4, 4])

    const $grid = grid.createGrid()
    const $container = $('#container');

    $container.html($grid)

    class Coordinate {
        x = 0;
        y = 0;

        constructor(x, y) {
            this.x = x;
            this.y = y;
        }

        update (x, y) {
            this.x = x;
            this.y = y;
        }
    }

    class Size {
        top;
        right;
        bottom;
        left;
        initW;
        initH;
        width;
        height;
        constructor($item) {
            this.initW = $item.attr('data-w');
            this.initH = $item.attr('data-h');

            this.width = $item.width()
            this.height = $item.height()

            if (!this.initW) {
                this.initW = this.width
                $item.attr('data-w', this.width)
            }
            if (!this.initH) {
                this.initH = this.height
                $item.attr('data-h', this.height)
            }

            console.log(this.width, this.height, this.initW, this.initH)
            this.update($item)
        }

        update = ($item) => {
            const { top, left } = $item.offset();
            this.top = top;
            this.right = left + this.width;
            this.bottom = top + this.height;
            this.left = left;
        }
    }

    class Position {
        type;
        index;
        // 设置当前操作覆盖的区域
        activated = new Set();
        $target;
        playground;
        size;
        start;
        end;
        move;

        constructor(type) {
            this.type = type;
        }

        setStart(event) {
            const $target = $(event.target).closest('.sullivan-grid-item-content');
            const activated = $target.attr('data-activated');
            this.index = $(event.target).closest('.sullivan-grid-item').index();
            // 目标元素
            this.$target = $target;
            // 开始位置
            this.start = new Coordinate(event.pageX, event.pageY);
            // 移动距离
            this.move = new Coordinate(0, 0);
            // 大小
            this.size = new Size(this.$target);
            // 背景板
            this.playground = new Size($(event.target).closest('.sullivan-grid'));

            this.activated.add(this.index)
            if (typeof activated === 'number') {
                this.activated.add(activated)
            } else {
                activated?.split(',')?.forEach(act => this.activated.add(+act))
            }

            // console.log('start: => ', event);
        }

        setMove(event) {
            const { left, right, top, bottom } = this.playground
            /**
             * 第一层限制，必须在九宫格范围内。
             */
            if (event.pageX >= left && event.pageX <= right && event.pageY >= top && event.pageY <= bottom) {
                const { left, right, top, bottom } = this.size
                /**
                 * 第二层限制，必须大于子区域大小。
                 */
                if (this.$target.height() >= this.size.initH || this.$target.width() >= this.size.initW) {
                    this.move.update(event.pageX - this.start.x, event.pageY - this.start.y);
                    this.updateView(event)
                }
            }
            // console.log('move: => ', this.$target.height(), this.size.initH)
        }

        setEnd(event) {
            this.end = new Coordinate(event.pageX, event.pageY);
            this.setMove(event);
            this.updateView();
            this.updateLayout();
            this.size.update(this.$target);
            console.log('end: => ', this)
        }

        updateView () {
            switch (this.type) {
                case 'top': {
                    this.$target.css({
                        height: this.size.height - this.move.y,
                        top: this.move.y
                    })
                    break
                }
                case 'right': {
                    this.$target.css({
                        width: this.size.width + this.move.x,
                    })
                    break
                }
                case 'bottom': {
                    this.$target.css({
                        height: this.size.height + this.move.y,
                    })
                    break
                }
                case 'left': {
                    this.$target.css({
                        width: this.size.width - this.move.x,
                        left: this.move.x
                    })
                    break
                }
            }
        }

        addActivatedForTop (moveDistance, index) {
            const idx = index - grid.row
            const itemArea = grid.areas[idx]

            console.log('data-activated', idx)
            this.activated.add(idx)
            grid.update(idx, grid.areas[this.index])
            if (!grid.areasCopy.includes(itemArea)) {
                grid.$grid.find(`.${itemArea}`).hide()
            }

            if (moveDistance > this.size.height) {
                this.addActivatedForTop(moveDistance - this.size.height, idx)
            }
        }

        removeActivatedForTop (moveDistance, index) {
            const idx = index + grid.row
            const itemArea = grid.areas[index]

            console.log('data-activated', index)
            this.activated.delete(index)
            grid.update(index, itemArea)
            if (grid.areas.includes(itemArea)) {
                grid.$grid.find(`.${itemArea}`).show()
            }

            if (moveDistance > this.size.height) {
                this.addActivatedForTop(moveDistance - this.size.height, idx)
            }
        }

        /**
         * 更新 areas
         *
         * @param { number } moveDistance 移动距离
         * @param { number } initSize 基础值，大于基础之，则会递归添加下一个
         * @param { number } gap 下一个间隔
         */
        updateGridAreas (moveDistance, initSize, gap) {
            const idx = index + gap
            console.log('data-activated', idx)
            this.activated.add(idx)
            grid.update(idx, grid.areas[this.index])
            if (moveDistance > size) {
                this.addActivatedForTop(moveDistance - size, idx)
            }
        }

        addActivatedForBottom (moveDistance, index) {
            const idx = index + grid.row
            const itemArea = grid.areas[idx]

            console.log('data-activated', idx)
            this.activated.add(idx)
            grid.update(idx, grid.areas[this.index])
            if (!grid.areasCopy.includes(itemArea)) {
                grid.$grid.find(`.${itemArea}`).hide()
            }

            if (moveDistance > this.size.height) {
                this.addActivatedForTop(moveDistance - this.size.height, idx)
            }
        }

        updateLayout () {
            switch (this.type) {
                case 'top': {
                    if (this.move.y < -20) {
                        this.addActivatedForTop(-this.move.y, Math.min(...this.activated.values()))
                        console.log('data-activated:enlarge => ', this.activated)
                    } else if (this.move.y > 20) {
                        this.removeActivatedForTop(this.move.y, Math.min(...this.activated.values()))
                        console.log('data-activated:shrink => ', this.activated, grid)
                    }
                    break
                }
                case 'right': {
                    break
                }
                case 'bottom': {
                    if (this.move.y < -20) {
                        this.addActivatedForBottom(this.move.y, Math.min(...this.activated.values()))
                        console.log('data-activated:enlarge => ', this.activated)
                    } else if (this.move.y > 20) {
                        this.addActivatedForBottom(this.move.y, Math.max(...this.activated.values()))
                        console.log('data-activated:shrink => ', this.activated, grid)
                    }
                    break
                }
                case 'left': {
                    break
                }
            }

            this.$target.removeAttr('style')
            this.$target.attr('data-activated', [...this.activated.values()])
        }
    }

    let position;


    const cursor = {
        set (cursor) {
            $(document.body).css('cursor', cursor + '-resize')
        },
        remove () {
            $(document.body).css('cursor', 'initial')
        }
    }

    function onTopMouseDown (event) {
        event.stopPropagation()
        cursor.set('row')
        position = new Position('top')
        position.setStart(event)
        bindMouseMove()
    }
    function onRightMouseDown (event) {
        event.stopPropagation()
        cursor.set('column')
        position = new Position('right')
        position.setStart(event)
        bindMouseMove()
    }
    function onBottomMouseDown (event) {
        event.stopPropagation()
        cursor.set('row')
        position = new Position('bottom')
        position.setStart(event)
        bindMouseMove()
    }

    function onLeftMouseDown (event) {
        event.stopPropagation()
        cursor.set('column')
        position = new Position('left')
        position.setStart(event)
        bindMouseMove()
    }

    // $container
    //     .off('mouseover', '.sullivan-grid-item', onMouseOver)
    //     .on('mouseover', '.sullivan-grid-item', onMouseOver)
    // $container
    //     .off('mouseout', '.sullivan-grid-item', onMouseOut)
    //     .on('mouseout', '.sullivan-grid-item', onMouseOut)
    function bindMouseMove () {
        $(document)
            .off('mousemove', onMouseMove)
            .on('mousemove', onMouseMove)
        $(document)
            .off('mouseup', onMouseUp)
            .on('mouseup', onMouseUp)
    }

    function offMouseMove () {
        // console.log('offMouseMove')
        cursor.remove()
        $container
            .off('mouseover', onMouseOver)
        $container
            .off('mouseout', onMouseOut)
        $(document)
            .off('mousemove', onMouseMove)
        $(document)
            .off('mouseup', onMouseUp)
    }

    function onMouseMove (event) {
        // console.log('onMouseMove')
        if (event.which) {
            position.setMove(event)
        } else {
            offMouseMove()
        }
    }

    function onMouseUp (event) {
        position?.setEnd(event);
        offMouseMove()
    }

    function onMouseOver (event) {
        // console.log('----------------onMouseOver--------------------', $(event.target).closest('.sullivan-grid-item').index())
        if (position) {
            position.activated.add($(event.target).closest('.sullivan-grid-item').index());
        }
    }

    function onMouseOut (event) {
        // console.log('----------------onMouseOut--------------------', $(event.target).closest('.sullivan-grid-item').index())
        if (position) {
            const index = $(event.target).closest('.sullivan-grid-item').index();
            if (index !== position.index) {
                position.activated.delete(index);
            }
        }
    }

    $container.on('mousedown', '.sullivan-grid-item-top', onTopMouseDown)
    $container.on('mousedown', '.sullivan-grid-item-right', onRightMouseDown)
    $container.on('mousedown', '.sullivan-grid-item-bottom', onBottomMouseDown)
    $container.on('mousedown', '.sullivan-grid-item-left', onLeftMouseDown)

</script>
</body>
</html>